/*
  Copyright 2012 Sergey Ostanin

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
*/

package ru.irenproject.store;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Map;

import javax.inject.Inject;
import javax.inject.Singleton;

import org.apache.commons.lang3.Validate;

import com.google.common.collect.ImmutableMap;
import com.google.inject.Injector;
import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.Message;
import com.google.protobuf.Message.Builder;
import com.google.protobuf.MessageOrBuilder;

@Singleton public final class ThingFactory {
  private static final String GET_DESCRIPTOR_METHOD_NAME = "getDescriptor";
  private static final String NEW_BUILDER_METHOD_NAME = "newBuilder";
  private static final String PARSE_FROM_METHOD_NAME = "parseFrom";

  private final Injector fInjector;
  private final ImmutableMap<Class<? extends AbstractThing>, StashDescriptor> fStashDescriptors;

  @Inject ThingFactory(ThingRegistry thingRegistry, Injector injector) {
    try {
      fInjector = injector;

      ImmutableMap.Builder<Class<? extends AbstractThing>, StashDescriptor> stashDescriptors = ImmutableMap.builder();

      for (Map.Entry<Class<? extends AbstractThing>, Descriptor> e : thingRegistry.getThingClasses().entrySet()) {
        Class<? extends AbstractThing> thingClass = e.getKey();
        Descriptor d = e.getValue();

        Field field = getStashField(thingClass);
        Class<? extends MessageOrBuilder> fieldClass = field.getType().asSubclass(MessageOrBuilder.class);

        Descriptor dd = (Descriptor) fieldClass.getMethod(GET_DESCRIPTOR_METHOD_NAME).invoke(null);
        Validate.isTrue(d == dd, "Invalid stash field type in %s: expected %s, found %s.",
            thingClass, d.getFullName(), dd.getFullName());

        boolean isBuilder = Builder.class.isAssignableFrom(fieldClass);
        Class<? extends Message> messageClass =
            (isBuilder ? fieldClass.getEnclosingClass() : fieldClass)
            .asSubclass(Message.class);
        field.setAccessible(true);

        stashDescriptors.put(thingClass, new StashDescriptor(field, isBuilder,
            messageClass.getMethod(NEW_BUILDER_METHOD_NAME),
            messageClass.getMethod(PARSE_FROM_METHOD_NAME, InputStream.class)));
      }

      fStashDescriptors = stashDescriptors.build();
    } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
      throw new RuntimeException(e);
    }
  }

  private static final class StashDescriptor {
    final Field fField;
    final boolean fIsBuilder;
    final Method fNewBuilderMethod;
    final Method fParseFromInputStreamMethod;

    StashDescriptor(Field field, boolean isBuilder, Method newBuilderMethod,
        Method parseFromInputStreamMethod) {
      fField = field;
      fIsBuilder = isBuilder;
      fNewBuilderMethod = newBuilderMethod;
      fParseFromInputStreamMethod = parseFromInputStreamMethod;
    }
  }

  private static Field getStashField(Class<? extends AbstractThing> cls) {
    Field res = null;
    for (Field f : cls.getDeclaredFields()) {
      if (f.isAnnotationPresent(Stash.class)) {
        Validate.isTrue(res == null, "Duplicate stash field in %s.", cls);
        Validate.isTrue(Modifier.isPrivate(f.getModifiers()), "Stash field is not private in %s.", cls);
        res = f;
      }
    }
    Validate.isTrue(res != null, "No stash field in %s.", cls);
    return res;
  }

  public <T extends AbstractThing> T create(Class<T> thingClass) {
    try {
      T res = fInjector.getInstance(thingClass);

      StashDescriptor d = findDescriptor(thingClass);
      if (d.fIsBuilder) {
        d.fField.set(res, d.fNewBuilderMethod.invoke(null));
      }

      return res;
    } catch (IllegalAccessException | InvocationTargetException e) {
      throw new RuntimeException(e);
    }
  }

  void loadStash(InputStream in, AbstractThing thing) {
    try {
      StashDescriptor d = findDescriptor(thing.getClass());
      if (d.fIsBuilder) {
        ((Builder) d.fField.get(thing)).mergeFrom(in);
      } else {
        d.fField.set(thing, d.fParseFromInputStreamMethod.invoke(null, in));
      }
    } catch (IllegalArgumentException | IllegalAccessException | IOException | InvocationTargetException e) {
      throw new RuntimeException(e);
    }
  }

  Message getStashMessage(AbstractThing thing) {
    try {
      StashDescriptor d = findDescriptor(thing.getClass());
      if (d.fIsBuilder) {
        return ((Builder) d.fField.get(thing)).build();
      } else {
        return (Message) d.fField.get(thing);
      }
    } catch (IllegalArgumentException | IllegalAccessException e) {
      throw new RuntimeException(e);
    }
  }

  void setStashMessage(AbstractThing thing, Message message) {
    try {
      StashDescriptor d = findDescriptor(thing.getClass());
      Validate.isTrue(!d.fIsBuilder);
      d.fField.set(thing, message);
    } catch (IllegalArgumentException | IllegalAccessException e) {
      throw new RuntimeException(e);
    }
  }

  private StashDescriptor findDescriptor(Class<? extends AbstractThing> thingClass) {
    StashDescriptor res = fStashDescriptors.get(thingClass);
    Validate.isTrue(res != null, "No stash descriptor for %s.", thingClass);
    return res;
  }
}
